//-----------------------------------------------------------------------
// <copyright file="MergeReports.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation.  All rights reserved.
// THIS CODE AND INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY 
// KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
// </copyright>
// <author>Simon Ransom</author>
// <email></email>
// <date>2004-03-23</date>
// <summary>Merge separate code coverage reports for the same assembly into a single report.</summary>
//-----------------------------------------------------------------------

namespace Microsoft.Sdc.Tasks.CodeCoverage
{
    using System;
    using System.Xml;
    using System.Text;
    using System.Globalization;
    using System.IO;
    using System.Text.RegularExpressions;
    using System.Collections;
    using Microsoft.Build.Framework;

    /// <summary>
    /// Merge separate code coverage reports for the same assembly into a single report.
    /// </summary>
    /// <remarks>
    /// <code><![CDATA[
    /// <CodeCoverage.MergeReports 
    ///     OutputPath="outputPath" 
    ///     FileSpec="fileSpec" 
    ///     ReportFilenameMatch="reportFilenameMatch" />]]></code>
    /// <para>where:</para>
    /// <para><i>outputPath (Required)</i></para>
    /// <para>The full path to the folder in which the Code Coverage reports can be found.</para>
    /// <para><i>fileSpec</i></para>
    /// <para>The search string to match against the names of files in OutputPath. 
    /// The parameter cannot end in two periods ("..") or contain two periods (".."). 
    /// The default value is "Report*.xml".</para>
    /// <para><i>reportFilenameMatch</i></para>
    /// <para>Regular expression providing additional filtering on the files found in <i>outputPath</i>
    /// that match the <i>fileSpec</i>. Defaults to all .xml files prefixed by "Report".
    /// </para>
    /// </remarks>
    /// <example>
    /// <code><![CDATA[
    /// <Project>
    ///     <Target Name="Test" >
    ///         <CodeCoverage.MergeReports
    ///             OutputPath="C:\CoverageEyeOutput"  
    ///             FileSpec="MyReport*.xml"  />
    ///     </Target>
    /// </Project>
    /// ]]></code>    
    /// </example>
    public class MergeReports : TaskBase
    {
        private string outputPath;
        private string fileSpec = "Report*.xml";
        private string reportFilenameMatch = @"Report.{[A-Z0-9-]*}.(?<dll>[a-zA-Z0-9.]+).xml";

        /// <summary>
        /// Initializes a new instance of the MergeReports class.
        /// </summary>
        public MergeReports()
        {
        }

        /// <summary>
        /// The full path to the folder in which the Code Coverage reports can be found.
        /// </summary>
        /// <value>The full path to the folder in which the Code Coverage reports can be found.</value>
        [Required]
        public string OutputPath
        {
            get
            {
                return (this.outputPath == null ? String.Empty : this.outputPath);
            }

            set
            {
                this.outputPath = value;
            }
        }

        /// <summary>
        /// The search string to match against the names of files in OutputPath. 
        /// The parameter cannot end in two periods ("..") or contain two periods (".."). 
        /// The default value is "Report*.xml"
        /// </summary>
        /// <seealso cref="Microsoft.Sdc.Tasks.Folder.Share.DeleteShare"/>
        /// <value>The search string to match against the names of files in OutputPath.</value>
        public string FileSpec
        {
            get
            {
                return this.fileSpec;
            }

            set
            {
                this.fileSpec = value;
            }
        }

        /// <summary>
        /// Regular expression providing additional filtering on the files found in <i>outputPath</i>
        /// that match the <i>fileSpec</i>. 
        /// </summary>
        /// <value><![CDATA[Regular expression providing additional filtering on the files found in <i>outputPath</i>
        /// that match the <i>fileSpec</i>. Defaults to "Report.{[A-Z0-9-]*}.(?<dll>[a-zA-Z0-9.]+).xml".]]></value>
        public string ReportFilenameMatch
        {
            get
            {
                return this.reportFilenameMatch;
            }

            set
            {
                this.reportFilenameMatch = value;
            }
        }

        /// <summary>
        /// Performs the action of this task.
        /// </summary>
        protected override void InternalExecute()
        {
            string[] sourceFiles = Directory.GetFiles(this.outputPath, this.fileSpec);
            Regex regExDll = new Regex(this.reportFilenameMatch);

            // First of all build up a merge list for each assembly
            Hashtable assemblyToArrayListOfFilenames = new Hashtable();
            foreach (string file in sourceFiles)
            {
                Match match = regExDll.Match(file);
                GroupCollection group = match.Groups;
                string dll = group["dll"].Value;
                if (assemblyToArrayListOfFilenames[dll] == null)
                {
                    assemblyToArrayListOfFilenames[dll] = new ArrayList();
                }
                ((ArrayList)assemblyToArrayListOfFilenames[dll]).Add(file);
            }

            // Now go through each list
            foreach (string assemblyName in assemblyToArrayListOfFilenames.Keys)
            {
                // Check there is more than one file otherwise no point in merging
                if (((ArrayList)(assemblyToArrayListOfFilenames[assemblyName])).Count > 1)
                {
                    XmlDocument resultDocument = null;
                    foreach (string file in ((ArrayList)assemblyToArrayListOfFilenames[assemblyName]))
                    {
                        // Get the file and merge it in
                        if (resultDocument == null)
                        {
                            // Load the first file as the base
                            resultDocument = new XmlDocument();
                            resultDocument.Load(file);
                        }
                        else
                        {
                            // Subsequent file to merge in
                            XmlDocument fileToMerge = new XmlDocument();
                            fileToMerge.Load(file);
                            this.Merge(fileToMerge, resultDocument);
                        }
                    }

                    if (resultDocument != null) // merging has happened - save out the result
                    {
                        // Recalculate the percentage covered
                        string filename = "Report.{" + Guid.NewGuid().ToString() + "}." + assemblyName + ".xml";
                        resultDocument.Save(Path.Combine(this.outputPath, filename));
                        Log.LogMessageFromResources("CodeCoverage.SavingReport", Path.Combine(this.outputPath, filename));

                        // Now delete the source files
                        foreach (string file in (ArrayList)assemblyToArrayListOfFilenames[assemblyName])
                        {
                            File.Delete(file);
                        }
                    }
                }
            }
        }

        private void Merge(XmlDocument sourceDocument, XmlDocument resultDocument)
        {
            XmlNodeList functions = sourceDocument.SelectNodes("//Function");
            XmlElement assemblyElement = (XmlElement)resultDocument.SelectSingleNode("/root/Assembly");

            int totalCoveredCount = 0;
            foreach (XmlElement function in functions)
            {
                // Find the corresponding function in the result document
                XmlElement correspondingFunction = (XmlElement)resultDocument.SelectSingleNode("//Function[@FunctionToken='" + function.GetAttribute("FunctionToken") + "']");

                // If it doesn't exist, simply copy the node over
                if (correspondingFunction == null)
                {
                    assemblyElement.AppendChild(resultDocument.ImportNode(function, true));
                }
                else
                {
                    // Otherwise merge the values
                    ArrayList resultCoveredIlOffsets = new ArrayList(correspondingFunction.GetAttribute("CoveredIlOffsets").Split(','));
                    string[] sourceCoveredIlOffsets = function.GetAttribute("CoveredIlOffsets").Split(',');

                    StringBuilder stringBuilderIlOffsetCsv = new StringBuilder();

                    int additionalCoverage = 0;
                    foreach (string source in sourceCoveredIlOffsets)
                    {
                        if (!resultCoveredIlOffsets.Contains(source))
                        {
                            stringBuilderIlOffsetCsv.Append(",");
                            stringBuilderIlOffsetCsv.Append(source);
                            additionalCoverage++;
                        }
                    }
                    correspondingFunction.SetAttribute(
                        "CoveredIlOffsets",
                        correspondingFunction.Attributes["CoveredIlOffsets"].Value + stringBuilderIlOffsetCsv.ToString());

                    // Update the CoveredCount
                    int newCoveredCount = Convert.ToInt32(correspondingFunction.Attributes["CoveredCount"].Value, CultureInfo.InvariantCulture) + additionalCoverage;
                    correspondingFunction.SetAttribute("CoveredCount", Convert.ToString(newCoveredCount, CultureInfo.InvariantCulture));

                    // and PercentageCovered
                    correspondingFunction.SetAttribute("PercentageCovered", Convert.ToString(Math.Round((Convert.ToDouble(newCoveredCount, CultureInfo.InvariantCulture) / Convert.ToDouble(correspondingFunction.Attributes["InstructionCount"].Value, CultureInfo.InvariantCulture)) * 100D), CultureInfo.InvariantCulture));

                    // increment the total covered count
                    totalCoveredCount += newCoveredCount;
                }
            }

            // Update the PercentageCovered in the Assembly element
            assemblyElement.SetAttribute("CoveredCount", Convert.ToString(totalCoveredCount, CultureInfo.InvariantCulture));

            // and CoveredCount 
            assemblyElement.SetAttribute(
                "PercentageCovered",
                Convert.ToString(
                Math.Round(
                    (Convert.ToDouble(totalCoveredCount, CultureInfo.InvariantCulture) /
                    Convert.ToDouble(assemblyElement.Attributes["InstructionCount"].Value, CultureInfo.InvariantCulture)) * 100D),
                    CultureInfo.InvariantCulture));
        }
    }
}